/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import static android.persist.op.sqlite.SQLLogging.SQL_LOG_TAG;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import android.content.ContentValues;
import android.database.sqlite.SQLiteDatabase;
import android.persist.PersistException;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;
import android.persist.op.Create;
import android.persist.op.OpException;
import android.reflect.util.ClassReflectionUtil;
import android.util.Log;

/**
 * {@code SQLiteCreate} inserts an object in the SQLite datastore using a SQL
 * INSERT statement. The tables required to store the object are already assumed
 * to have been created by {@link SQLiteCreateStowage}.
 * <p>
 * Here is an example of how to use this operation:
 * <p>
 * <blockquote>
 * 
 * <pre>
 * 
 * </pre>
 * </blockquote>
 * 
 * @author Richard Schilling
 * @param <OT> the type of class to save
 * @since 1.0
 */
public class SQLiteCreate<OT> extends Create<OT, Long> {

    /**
     * The database that this operation will be applied against.
     * 
     * @since 1.0
     */
    protected final SQLiteDatabase mDatabase;

    /**
     * Configuration information about the operation's type.
     * 
     * @since 1.0
     */
    protected final ClassConfig<OT> mConfig;

    /**
     * Creates a new operation.
     * 
     * @param db the database to persist {@code o} into.
     * @param o the object to persist
     * @throws IllegalArgumentException if db or o is null.
     * @throws OpException if db is not writeable.
     */
    @SuppressWarnings("unchecked")
    public SQLiteCreate(SQLiteDatabase db, OT o) {
        super(o);
        if (db == null) {
            throw new IllegalArgumentException("db cannot be null");
        }

        if (db.isReadOnly()) {
            throw new OpException("database must be writeable");
        }

        if (o == null) {
            throw new IllegalArgumentException("o cannot be null");
        }

        mDatabase = db;
        mConfig = (ClassConfig<OT>) ClassConfig.getConfig((Class<OT>) operand.getClass());
    }

    @SuppressWarnings({
            "unchecked", "rawtypes"
    })
    /**
     * Inserts data for the object passed to the constructor as well as its fields.  
     * The number returned is the unique ROWID that the object was stored with.
     * @author Richard Schilling
     * @since 1.0
     * @return the SQLite ROWID value that the object was stored under.
     * @throws SQLiteException if the SQL operation fails
     * @throws OpException if the database is closed.
     * 
     */
    @Override
    public Long call() {

        Long result = this.createOperand();

        // get the list of operations to save foreign references.
        List<SQLiteCreateForeign<?>> ops = getCreateOps();
        for (SQLiteCreateForeign op : ops) {

            if (op instanceof SQLiteCreateCollection) {

                SQLiteCreateCollection collectionOp = (SQLiteCreateCollection) op;
                collectionOp.parentROWID = result;
                collectionOp.call();

            } else {
                // save the foreign reference data in its own table.
                Long rowId = op.createOperand();

                // update the field in the current data store with the reference
                // identifier.
                Map<FieldConfig, Object> values = new HashMap<FieldConfig, Object>();
                values.put(op.mFieldConfig, rowId);

                // update the key column
                SQLiteUpdate<OT> updateOp = new SQLiteUpdate(mDatabase, operand, values);
                Integer callResult = updateOp.call();
                if (callResult != 1)
                    throw new PersistException("unable to persist foreign key "
                            + op.mFieldConfig.getPersistedName() + ". " + callResult
                            + " rows updated (expected 1)");

            }

        }

        return result;

    }

    public Long createOperand() {

        if (!mDatabase.isOpen()) {
            throw new OpException("database is closed");
        }

        ContentValues values = new ContentValues();
        Map<FieldConfig, Object> domesticValues = mConfig.getDomesticFieldValues(operand);
        for (Entry<FieldConfig, Object> entry : domesticValues.entrySet()) {
            SQLiteUtil.addContentValue(entry.getKey().getPersistedName(), entry.getValue(), values);
        }

        if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
            Log.d(SQL_LOG_TAG,
                    "INSERT INTO " + mConfig.storageName + ": " + values.toString());
        }
        try {
            return mDatabase.insertOrThrow(mConfig.storageName, null, values);
        } catch (Throwable t) {
            throw new RuntimeException("unable to insert into " + mConfig.storageName
                    + " with values " + values.toString(), t);
        }

    }

    /**
     * Reads through all of the fields of a class and generates a list of
     * operations for each one that is not null.
     * 
     * @return a list of operations that can be applied to save the fields of
     *         the operand.
     * @since 1.0
     */
    @SuppressWarnings({
            "unchecked", "rawtypes"
    })
    private List<SQLiteCreateForeign<?>> getCreateOps() {
        List<SQLiteCreateForeign<?>> result = new ArrayList<SQLiteCreateForeign<?>>();

        ClassConfig<?> cfg = ClassConfig.getConfig(operand.getClass());

        Map<FieldConfig, Object> foreignValues = cfg.getForeignFieldValues(operand);
        for (Entry<FieldConfig, Object> entry : foreignValues.entrySet()) {
            if (entry.getKey().isCollection()) {
                Collection<?> collection = (Collection<?>) entry.getValue();
                if (collection == null || collection.size() == 0) {
                    continue;
                }

                result.add(new SQLiteCreateCollection(mDatabase, entry));
            } else {
                result.add(new SQLiteCreateForeign(mDatabase, entry));
            }

        }

        return result;

    }

    /**
     * An operation that is used to save foreign values.
     * 
     * @author Richard Schilling
     * @param <FT> The class type of the foreign field that is being stored in
     *            the data store.
     * @since 1.0
     */
    private static class SQLiteCreateForeign<FT> extends SQLiteCreate<FT> {

        public final FieldConfig mFieldConfig;

        public SQLiteCreateForeign(SQLiteDatabase db, Entry<FieldConfig, FT> entry) {
            super(db, entry.getValue());
            mFieldConfig = entry.getKey();

        }

    }

    /**
     * Inserts a collection field into its child table. If the collection
     * generic type is a primitive, then the value is saved directly to the
     * child table. If the collection generic type then the value is saved to
     * its own table and the row id is saved in the child table.
     * 
     * @author Richard Schilling
     * @param <BT>
     */
    private static class SQLiteCreateCollection<BT> extends SQLiteCreateForeign<BT> {

        public Long parentROWID;

        public SQLiteCreateCollection(SQLiteDatabase db,
                Entry<FieldConfig, BT> entry) {
            super(db, entry);

            if (!mFieldConfig.isCollection()) {
                throw new IllegalArgumentException("FieldConfig (key) must be a for a Collection");
            }

            if (operand == null) {
                throw new UnsupportedOperationException("saving null collections is not supported");
            }

            if (((Collection<?>) operand).size() == 0) {
                throw new UnsupportedOperationException("cannot save empty collections");
            }

        }

        @Override
        public Long call() {

            if (!mDatabase.isOpen()) {
                throw new OpException("database is closed");
            }

            if (parentROWID == null) {
                throw new IllegalStateException("parentROWID has not been set");
            }

            Collection<?> collection = (Collection<?>) operand;

            StringBuilder tableName = new StringBuilder();
            tableName.append(mFieldConfig.datastoreName);
            tableName.append("_");
            tableName.append(mFieldConfig.getPersistedName());

            Class<?> columnTypeClass = mFieldConfig.getCollectionType();

            @SuppressWarnings({
                    "unchecked", "rawtypes"
            })
            ContentValues values = new ContentValues();

            if (ClassReflectionUtil.isPrimitiveOrArray(columnTypeClass)) {
                for (Object o : collection) {
                    values.put("parent_ROWID", parentROWID);
                    // save the value as-is.
                    SQLiteUtil.addContentValue(mFieldConfig.getPersistedName(), o, values);

                    if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                        Log.d(SQL_LOG_TAG,
                                "INSERT INTO  "
                                        + tableName.toString()
                                        + ": " + values.toString());
                    }
                    mDatabase.insertOrThrow(tableName.toString(), null,
                            values);

                    values.clear();

                }
            } else {

                for (Object o : collection) {
                    values.put("parent_ROWID", parentROWID);

                    /*
                     * save the foreign type in its own table and retrieve the
                     * ROWID of the inserted value.
                     */
                    @SuppressWarnings({
                            "rawtypes", "unchecked"
                    })
                    SQLiteCreate<?> createOp = new SQLiteCreate(mDatabase, o);
                    Long rowId = createOp.call();
                    SQLiteUtil.addContentValue(mFieldConfig.getPersistedName(), rowId, values);

                    if (Log.isLoggable(SQL_LOG_TAG, Log.DEBUG)) {
                        Log.d(SQL_LOG_TAG,
                                "INSERT INTO "
                                        + tableName.toString()
                                        + ": " + values.toString());
                    }
                    mDatabase.insertOrThrow(tableName.toString(), null,
                            values);

                    values.clear();
                }

                if (values.size() != 0) {
                    throw new IllegalStateException("values did not clear properly.");
                }

            }

            return null;

        }
    }

}
